#include <stdio.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <linux/input.h>
#include <string.h>
#include "xdg-shell-client-protocol.h"


#include <glib.h>

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/pixdesc.h"
#include "libavutil/hwcontext.h"
#include "libavutil/opt.h"
#include "libavutil/avassert.h"
#include "libavutil/imgutils.h"

#define VERTS_ARRAY_SIZE (sizeof (GLfloat) * 4 * 4)
#define TEX_ARRAY_SIZE (sizeof (GLfloat) * 4 * 2)

struct wl_display *display = NULL;
struct wl_registry *reg;
struct wl_compositor *compositor;
struct wl_shell *shell;
struct wl_surface *surface;
//struct wl_shell_surface *shell_surface;
struct wl_region *region;

//extension protocol,stable version
struct xdg_wm_base *xdg_shell;
struct xdg_surface *shell_surface;
struct xdg_toplevel *toplevel;

//mouse input
struct wl_seat *seat;
struct wl_pointer *pointer;

EGLDisplay  egl_display;
EGLConfig   egl_config;
EGLSurface  egl_surface;
EGLContext  egl_context;
EGLImageKHR egl_image;
GLuint egl_prog;
GLuint egl_tex_id;
gint egl_mproj;
gint egl_attr_pos;
gint egl_attr_tex;
guint egl_vbuf_id;

guint init_flg = 0;

struct wl_egl_window *egl_window;

const int WIDTH = 1920;
const int HEIGHT = 1080;

const char *filename;
const AVCodec *codec = NULL;
AVCodecParserContext *parser;
AVCodecContext *c = NULL;

#define INBUF_SIZE 4096
FILE *f;
AVFrame *frame;
uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
uint8_t *data;
size_t data_size;

int ret;
int eof;
AVPacket *pkt;
//gl draw nv21
GLchar * egl_vs_2 [] =
{
    "attribute vec4 position; \n"
    "attribute vec2 texcoords; \n"
    "varying vec2 tcoords; \n"
    "uniform mat4 mproj; \n "
    "void main () {\n"
    "   gl_Position = mproj * position; \n"
    "   tcoords = texcoords; \n"
    "}\n"
};

GLchar * egl_fs_2 [] =
{
    "#extension GL_OES_EGL_image_external : require\n"
    "precision mediump float; \n"
    "varying vec2 tcoords; \n"
    "uniform samplerExternalOES sampler; \n"
    "void main () {\n"
    "   gl_FragColor = texture2D (sampler, tcoords) ; \n"
    "}\n"
};

//mouse input
    static void
pointer_handle_enter(void *data, struct wl_pointer *pointer,
        uint32_t serial, struct wI_surface *surface,
        wl_fixed_t sx, wl_fixed_t sy)
{}

    static void
pointer_handle_leave(void *data, struct wl_pointer *pointer,
        uint32_t serial, struct wl_surface *surface)
{}

    static void
pointer_handle_motion(void *data, struct wl_pointer *pointer,
        uint32_t time, wl_fixed_t sx, wl_fixed_t sy)
{}

    static void
pointer_handle_button(void *data, struct wl_pointer *wl_pointer,
        uint32_t serial, uint32_t time, uint32_t button, uint32_t state)
{
    if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED)
    {
        xdg_toplevel_move(toplevel, seat, serial);
    }
}

    static void
pointer_handle_axis(void *data, struct wl_pointer *wl_pointer,
        uint32_t time, uint32_t axis, wl_fixed_t value)
{
}

static const struct wl_pointer_listener pointer_listener = {
    pointer_handle_enter,
    pointer_handle_leave,
    pointer_handle_motion,
    pointer_handle_button,
    pointer_handle_axis,
};

    static void
seat_handle_capabilities(void *data, struct wl_seat *seat,
        enum wl_seat_capability caps)
{
    if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer)
    {
        pointer = wl_seat_get_pointer(seat);
        wl_pointer_add_listener(pointer, &pointer_listener, NULL);
    } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && pointer) {
        wl_pointer_destroy (pointer);
        pointer = NULL;
    }
}

static const struct wl_seat_listener seat_listener = {
    seat_handle_capabilities,
};

//xdg shell
static void handle_configure(void *data, struct xg_surface *surface, uint32_t serial) {
    xdg_surface_ack_configure(surface, serial);
}

static const struct xdg_surface_listener surface_listener = {
    handle_configure
};

//windows handle
    static void
xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, uint32_t serial)
{
    xdg_wm_base_pong(shell, serial);
}

static const struct xdg_wm_base_listener wm_base_listener = {
    xdg_wm_base_ping,
};

//global registry
static void registry_handler(void *data, struct wl_registry *registry, uint32_t id,
        const char *interface, uint32_t version)
{
    if (strcmp(interface, "wl_compositor") == 0)
    {
        compositor = wl_registry_bind(registry, id, &wl_compositor_interface, 1);
    }
    else if (strcmp(interface, xdg_wm_base_interface.name) == 0)
    {
        xdg_shell = wl_registry_bind(registry, id, &xdg_wm_base_interface, 1);
        xdg_wm_base_add_listener(xdg_shell, &wm_base_listener, 1);
    }
    else if (strcmp(interface, "wl_seat") == 0)
    {
        seat = wl_registry_bind(registry, id, &wl_seat_interface, 1);
        wl_seat_add_listener(seat, &seat_listener, NULL);
    }
}

static void registry_remover (void *data, struct wl_registry *registry, uint32_t id)
{}

static const struct wl_registry_listener registry_listener= {
    registry_handler,
    registry_remover
};

int create_window()
{
    display = wl_display_connect(0);

    if (!display)
    {
        g_print("wl: display connect err\n");
        return -1;
    }

    reg = wl_display_get_registry(display);
    if (!reg)
    {
        g_print("wl: get registry err\n");
        return -1;
    }

    wl_registry_add_listener (reg, &registry_listener, NULL);

    wl_display_dispatch(display);
    wl_display_roundtrip(display);

    if (compositor == NULL)
    {
        g_print("wl: get compositor err\n");
        return -1;
    }

    if (xdg_shell == NULL)
    {
        g_print("wl: get xdg shell err\n");
        return -1;
    }

    surface = wl_compositor_create_surface(compositor);
    if (surface == NULL)
    {
        g_print("wl: create surface err\n");
        return -1;
    }

    shell_surface = xdg_wm_base_get_xdg_surface(xdg_shell, surface);
    toplevel = xdg_surface_get_toplevel(shell_surface);
    xdg_surface_add_listener(shell_surface, &surface_listener, NULL);

    xdg_toplevel_set_title(toplevel, "wayland-simple");
    xdg_toplevel_set_app_id(toplevel, "wayland-simple");

    region = wl_compositor_create_region(compositor);
    wl_region_add(region, 100, 100, WIDTH, HEIGHT);
    wl_surface_set_opaque_region(surface, region);

    return 0;
}

int init_egl()
{
    // init egl
    EGLint major, minor, config_count, tmp_n;
    EGLint config_attribs [] = {
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 0,
        EGL_NONE
    };
    EGLint context_attribs [] = {
#ifdef EGL_CONTEXT_MAJOR_VERSION
        EGL_CONTEXT_MAJOR_VERSION, 3,
#else
        EGL_CONTEXT_CLIENT_VERSION, O,
#endif
        EGL_NONE
    };

    egl_display = eglGetDisplay((EGLNativeDisplayType)display);
    eglInitialize(egl_display, &major, &minor);
    eglGetConfigs(egl_display, 0, 0, &config_count);
    eglChooseConfig(egl_display, config_attribs, &(egl_config), 1, &tmp_n);
    egl_context = eglCreateContext (
            egl_display,
            egl_config,
            EGL_NO_CONTEXT,
            context_attribs);

    egl_window = wl_egl_window_create(surface, WIDTH, HEIGHT);
    egl_surface = eglCreateWindowSurface (
            egl_display,
            egl_config,
            egl_window,
            0);
    eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);

    g_print("gl vendor %s\n", glGetString(GL_VENDOR));
    g_print("gl render %s\n", glGetString(GL_RENDERER));

    glClearColor(0.5f, 0.5f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    eglSwapBuffers(egl_display, egl_surface);

    return 0;
}


static void apply_ortho (guint mproj, float left, float right,
        float bottom, float top, float near, float far)
{
    float a = 2.0f / (right - left);
    float b = 2.0f / (top - bottom);
    float c = -2.0f / (far - near);

    float tx = - (right + left) / (right - left);
    float ty = - (top + bottom) / (top - bottom);
    float tz = - (far + near) / (far - near);

    float ortho [16] = {
        a, 0, 0, 0,
        0, b, 0, 0,
        0, 0, c, 0,
        tx, ty, tz, 1
    };
    glUniformMatrix4fv (mproj, 1, GL_FALSE, &ortho[0]);
}

int init_shader()
{
    GLuint fs = 0;
    GLuint vs = 0;
    GLuint buf;
    GLint status;
    GLint tex_loc;
    GLint prog;
    gboolean success = FALSE;
    gchar log[1000] = {0};
    GLsizei len;

    glGetIntegerv(GL_CURRENT_PROGRAM, &prog);
    fs = glCreateShader(GL_FRAGMENT_SHADER);
    if (fs == 0) {
        g_warning ("%s %d %d", __FUNCTION__, __LINE__, fs);
        for (GLint error = glGetError(); error; error = glGetError ())
            g_warning("glError GL_FRAGMENT_SHADER (0x%x)", error);
    }
    glShaderSource(fs, 1, (const char **)&egl_fs_2, NULL);


    glCompileShader(fs);
    glGetShaderiv(fs, GL_COMPILE_STATUS, &status);
    if (!status) {
        glGetShaderInfoLog(fs, sizeof(log), &len, log);
        g_warning("failed to compile fragment shader: %d %s", fs, log);
        goto end;
    }

    vs = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vs, 1, (const char **)&egl_vs_2, NULL) ;
    glCompileShader(vs);
    glGetShaderiv (vs, GL_COMPILE_STATUS, &status) ;
    if (!status) {
        glGetShaderInfoLog(vs, sizeof(log), &len, log);
        g_warning("failed to compile vertex shader: %s", log);
        goto end;
    }

    egl_prog = glCreateProgram();
    glAttachShader(egl_prog, fs);
    glAttachShader(egl_prog, vs);
    glLinkProgram(egl_prog);
    glGetProgramiv(egl_prog, GL_LINK_STATUS, &status);
    if (!status) {
        glGetProgramInfoLog(egl_prog, sizeof(log), &len, log);
        g_warning("error linking shaders: %s", log);
        goto end;
    }

    glUseProgram(egl_prog);
    glDetachShader(egl_prog, fs);
    glDetachShader(egl_prog, vs);

    egl_attr_pos = glGetAttribLocation(egl_prog, "position");
    egl_attr_tex = glGetAttribLocation(egl_prog, "texcoords");
    tex_loc = glGetUniformLocation(egl_prog, "samp");
    egl_mproj = glGetUniformLocation(egl_prog, "mproj");
    apply_ortho(egl_mproj, 0, WIDTH, 0, HEIGHT, -1, 1);

    glUniform1i(tex_loc, 0);

    glGenVertexArrays(1, &buf);
    glBindVertexArray(buf);

    glGenBuffers(1, &buf);
    glBindBuffer(GL_ARRAY_BUFFER, buf);

    glBufferData(GL_ARRAY_BUFFER,
            VERTS_ARRAY_SIZE + TEX_ARRAY_SIZE,
            NULL,
            GL_STATIC_DRAW);
    egl_vbuf_id = buf;

    glGenTextures(1, &egl_tex_id);
    success = TRUE;
end:
    if (fs) {
        glDeleteShader(fs);
    }
    if (vs) {
        glDeleteShader(vs);
    }
    glUseProgram (prog);

    return success;
}

static void draw_rect_from_arrays(const void *verts, const void *tex)
{
    glBindBuffer(GL_ARRAY_BUFFER, egl_vbuf_id);

    if (verts) {
        glEnableVertexAttribArray(egl_attr_pos);
        glBufferSubData(GL_ARRAY_BUFFER,
                0,
                VERTS_ARRAY_SIZE,
                verts);
        glVertexAttribPointer(egl_attr_pos, 4, GL_FLOAT,
                GL_FALSE, 0, 0);
    }
    if (tex) {
        glEnableVertexAttribArray(egl_attr_tex);
        glBufferSubData(GL_ARRAY_BUFFER,
                VERTS_ARRAY_SIZE,
                TEX_ARRAY_SIZE,
                tex);
        glVertexAttribPointer(egl_attr_tex, 2, GL_FLOAT,
                GL_FALSE, 0,
                (void *)VERTS_ARRAY_SIZE);

    }
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    if (verts)
        glDisableVertexAttribArray(egl_attr_pos);
    if (tex)
        glDisableVertexAttribArray(egl_attr_tex);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

static GLvoid client_draw_rect_tex(float x, float y, float w, float h,
        float tx, float ty, float tw, float th)
{
    GLfloat tex[4][2] = {
        { tx, ty },
        { tx + tw, ty},
        { tx, ty + th},
        { tx + tw, ty + th},
    };

    GLfloat verts[4][4] = {
        { x, y, 0.0, 1.0},
        { x + w, y, 0.0, 1.0},
        { x , y + h, 0.0, 1.0},
        { x + w, y + h, 0.0, 1.0},
    };

    draw_rect_from_arrays(verts, tex);
}

static void display_avframe_oes(AVFrame *frame)
{
    EGLint eglImgAttrs [30];
    GLenum target = GL_TEXTURE_EXTERNAL_OES;
    EGLint format = WL_SHM_FORMAT_NV21;
    int n_planes = 2;
    int atti = 0;
    int dmabuffd = *((long*) frame->opaque);
    int prog;
    if (init_flg == 0) {
        init_egl();
        init_shader();
        init_flg = 1;
    }

    PFNEGLCREATEIMAGEKHRPROC    eglCreateImageKHR = eglGetProcAddress("eglCreateImageKHR");
    PFNEGLDESTROYIMAGEKHRPROC   eglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR");


    if (egl_image != NULL) {
        eglDestroyImageKHR(egl_display, egl_image);
        egl_image = NULL;
    }

    eglImgAttrs[atti++] = EGL_WIDTH;
    eglImgAttrs[atti++] = frame->width;
    eglImgAttrs[atti++] = EGL_HEIGHT;
    eglImgAttrs[atti++] = frame->height;
    eglImgAttrs[atti++] = EGL_LINUX_DRM_FOURCC_EXT;
    eglImgAttrs[atti++] = format;

    if (n_planes > 0) {
        eglImgAttrs[atti++] = EGL_DMA_BUF_PLANE0_FD_EXT;
        eglImgAttrs[atti++] = dmabuffd;
        eglImgAttrs[atti++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
        eglImgAttrs[atti++] = 0;
        eglImgAttrs[atti++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
        eglImgAttrs[atti++] = frame->width;
    }

    if (n_planes > 1) {
        eglImgAttrs[atti++] = EGL_DMA_BUF_PLANE1_FD_EXT;
        eglImgAttrs[atti++] = dmabuffd;
        eglImgAttrs[atti++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT;
        eglImgAttrs[atti++] = frame->width * frame->height;
        eglImgAttrs[atti++] = EGL_DMA_BUF_PLANE1_PITCH_EXT;
        eglImgAttrs[atti++] = frame->width;
    }
    eglImgAttrs [atti++] = EGL_NONE;

    egl_image = eglCreateImageKHR(egl_display,
            EGL_NO_CONTEXT,
            EGL_LINUX_DMA_BUF_EXT,
            NULL,
            eglImgAttrs);
	if(!egl_image || egl_image == EGL_NO_IMAGE_KHR)
	{
		printf("eglCreateImageKHR ret:%d fd:%x\n", glGetError(), dmabuffd);
	}
    eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
    glBindTexture(target, egl_tex_id);
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glEGLImageTargetTexture2DOES(target, egl_image);
    glDisable(GL_BLEND);
    glGetIntegerv(GL_CURRENT_PROGRAM, &prog);
    glUseProgram(egl_prog);
    client_draw_rect_tex(0, 0, WIDTH, HEIGHT, 0, 1, 1, -1);
    eglSwapBuffers(egl_display, egl_surface);
    glUseProgram(prog);
    glFlush();
    return 1;
}



static void decode (AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt)
{
    int ret;
    GDateTime *start, *end;
    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0) {
        g_print("Error sending a packet for decoding\n");
        exit (1);
    }
    start = g_date_time_new_now_local();
    //g_print("start time (m:s:us):%02d:%02d:%06d\n", g_date_time_get_minute(start), g_date_time_get_second(start), g_date_time_get_microsecond(start));
    g_date_time_unref(start);

    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            g_print("Error during decoding\n");
            exit(1);
        }

        display_avframe_oes(frame);

        end = g_date_time_new_now_local();
        //g_print ("end time (m:s:us):%02d:%02d:%06d\n" ,g_date_time_get_minute(end), g_date_time_get_second(end), g_date_time_get_microsecond(end));
        g_date_time_unref (end);
        g_usleep (20*1000);
    }
}


void *thread_video_play(void *data)
{
    do {
        data_size = fread(inbuf, 1, INBUF_SIZE, f);
        if (ferror(f))
            break;
        eof = !data_size;

        data = inbuf;
        while (data_size > 0 || eof) {
            ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size,
                    data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                g_print("Error while parsing\n");
                exit(1);
            }
            data      += ret;
            data_size -= ret;

            if (pkt->size)
                decode (c, frame, pkt);
            else if (eof)
                break;
        }
    } while (!eof);

    /* flush the decoder */
    decode(c, frame, NULL);

    fclose(f);

    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);
}

static enum AVPixelFormat hw_pix_fmt = AV_PIX_FMT_DRM_PRIME;

static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *pix_fmts)
{
    const enum AVPixelFormat *p;

    for (p = pix_fmts; *p != -1; p++) {
        if (*p == hw_pix_fmt)
            return *p;
    }
    g_info("Failed to get HW surface format. \n");
    return AV_PIX_FMT_NONE;
}

int init_video ()
{
    avcodec_register_all();

    f = fopen(filename, "rb");
    if (!f)
    {
        g_print("vo open file err \n");
        return -1;
    }

    pkt = av_packet_alloc();
    if (!pkt)
    {
        g_print("vo pkt err \n");
        return -1;
    }

    codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if (!codec)
    {
        g_print("vo codec err \n");
        return -1;
    }

    parser = av_parser_init(codec->id);
    if (!parser)
    {
        g_print ("vo parser err \n");
        return -1;
    }

    c = avcodec_alloc_context3(codec);
    if (!c)
    {
        g_print("vo context err \n");
        return -1;
    }

    c->thread_count = 4;
    c->flags |= AV_CODEC_FLAG_LOW_DELAY;
    c->get_format = get_hw_format;

    AVBufferRef *hw_device_ctx = NULL;
    if ((av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_DRM,
                    "/dev/dri/card0", NULL, 0)) < 0) {
        g_print("Failed to create specified HW device %d.", AV_HWDEVICE_TYPE_DRM);
        exit(1);
    }
    c->hw_device_ctx = hw_device_ctx;

    if (avcodec_open2 (c, codec, NULL) < 0)
    {
        g_print("vo open codec err\n");
        return -1;
    }

    frame = av_frame_alloc();
    if (!frame)
    {
        g_print("vo frame error\n");
        return -1;
    }
    return 0;
}

int init()
{
    if (init_video() != 0) return -1;
    if (create_window() != 0) return -1;
    return 0;
}

int run()
{
    g_thread_new(NULL, thread_video_play, NULL);
    while (wl_display_dispatch(display) != -1)
    {}
    wl_display_disconnect(display);

    return 0;
}


int main(int argc, char **argv)
{
    if (argc != 2) {
        printf("No h264 file\n");
        exit(0);
    }

    filename = argv[1];

    if (init() != 0) return -1;

    run();
    end();
    return 0;
}
